#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         FileUpload.py
#
# Copyright:    Copyright (c) 2005, Faulkner Technologies
#
# Author:       James Davies
#
# Description:  A FileUpload is a simple Component that permits uploading
#               a file during an assessment
#####################################################################

#####################################################################
## Knowledge Engine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Component import Component, ComponentInfo
from Products.KnowledgeEngine.Assessible import Assessible
from Products.KnowledgeEngine.AssessmentComponentState import AssessmentComponentState
from Products.KnowledgeEngine.AssessmentComponentValueTypes import *
from Products.KnowledgeEngine.Exportable import Exportable
from Products.KnowledgeEngine.KERLExtensions import KERLExtensions
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.KERFProcessor import *
from Products.KnowledgeEngine.KERLProcessor import InvalidKerlOperator

#####################################################################
## Plone tools
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.DirectoryView import _dirreg, registerFileExtension
from Products.CMFCore.FSFile import FSFile

#####################################################################
## Zope Library Imports
from Globals import DTMLFile
from App.ImageFile import ImageFile

#####################################################################
## Python Library Imports
import os, os.path, stat, types


#####################################################################
## MIME Type Dictionary ... used to lookup appropriate file suffix
MIME_IMAGE = {
'image/gif'             : 'gif',
'image/jpeg'            : 'jpg',
'image/jpg'             : 'jpg',
'image/png'             : 'png',
}

MIME_APPLICATION = {
    'application/pdf'       : 'pdf',
    'application/msword'    : 'doc',
    'application/pdf'       : 'pdf',
    'application/x-pdf'     : 'pdf',
}

MIME_MS_EXCEL = {
    'application/msexcel'           : 'xls',
    'application/x-msexcel'         : 'xls',
    'application/x-ms-excel'        : 'xls',
    'application/vnd.ms-excel'      : 'xls',
    'application/x-excel'           : 'xls',
    'application/x-dos_ms_excel'    : 'xls',
    'application/xls'               : 'xls',
    'application/x-xls'             : 'xls',
}

MIME_TYPES = {}
MIME_TYPES.update(MIME_IMAGE)
MIME_TYPES.update(MIME_APPLICATION)


#####################################################################
## FileUploadInfo Class Definition
class FileUploadInfo(ComponentInfo):
    """
        Defines the class info methods, defining information about
        the class without needing an instance of the class
    """

    def getClassName(self):
        """
            Return the class name
        """
        
        return 'FileUpload'


    #-------------------------------------------------------------------------
    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """
    
        return "File Upload"


    #-------------------------------------------------------------------------
    def getIdentityType(self):
        """
            Return the identity type.  
        """
        
        return "file"


    #-------------------------------------------------------------------------
    def getIcon(self, relative_to_portal=0):
        """
            PLONE Support:
            
            Returns a URL to the icon for the object
        """
                
        return self.getClassName() + '_icon.png'


    #-------------------------------------------------------------------------
    def getClassIcon(self):
        """
            Return the appropriate icon file for the class.
        """
        
        return ImageFile('skins/%s_icon.png' % self.getClassName(), globals())


    #-------------------------------------------------------------------------
    def isInstanceOf(self, className):
        """
            Returns if the class is an instance of the specified
            className, or implements the interface specified 
            by the className
        """

        return className in ('Assessible', 'Exportable') or ComponentInfo.isInstanceOf(self, className)


    #-------------------------------------------------------------------------
    def onConfigureProperties(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific properties using the PropertyReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """

        #initialise the super-class properties for reflection
        ComponentInfo.onConfigureProperties(self)
        
        #add the 'reportContent' and 'filesystemDirectory' properties
        self.addPropertyDefinition(
            PropertyDefinition(
                identity="reportContent",
                displayName="Report Content",
                description="Text to be displayed for this component when used in a report",
                defaultValue="",
                propertyType=pt_STRING,
                propertyFormat=pf_HTML,
                propertyStructure=ps_ATOMIC
            )
        )
        
        self.addPropertyDefinition(
            PropertyDefinition(
                identity="filesystemDirectory",
                displayName="Filesystem Directory Location",
                description="Absolute directory where files will be uploaded. The uploaded file will be stored in this directory under a name composed of the assessment and componentid. Please ask your friendly neighbourhood systems administrator if unsure.",
                defaultValue="/tmp",
                propertyType=pt_STRING,
                propertyFormat=pf_TEXT_SINGLELINE,
                propertyStructure=ps_ATOMIC
            )
        )


        self.addPropertyDefinition(
            PropertyDefinition(
                identity="allowedMimeTypes",
                displayName="Allowable MIME types",
                description="A comma seperated list of MIME type which are permitted to be uploaded",
                #defaultValue=",".join(MIME_TYPES.keys()),
                defaultValue="",
                propertyType=pt_STRING,
                propertyFormat=pf_TEXT_SINGLELINE,
                propertyStructure=ps_ATOMIC
            )
        )

        #add the 'maximumSize' property
        self.addPropertyDefinition(
            PropertyDefinition(
                identity="maximumFilesize",
                displayName="Maximum File Size",
                description="The maximum size allowed to be uploaded (in KiB)",
                defaultValue=1024L,
                propertyType=pt_INTEGER,
                propertyFormat=pf_NATIVE,
                propertyStructure=ps_ATOMIC                                                        
            )
        )


#####################################################################
## FileUpload Class Definition
class FileUpload(Component, Assessible, KERLExtensions, Exportable):
    """
        Defines a simple component to represent one or more lines of 
        text entered during an assessment.
    """

    #TRANSIENT: a ClassInfo implementation instance to provide info about this class
    _CLASSINFO = FileUploadInfo()


    # DTML Methods
    index_html = DTMLFile('dtml/index_html', globals())
    simplereport_html = DTMLFile('dtml/simplereport_html', globals())


    #-------------------------------------------------------------------------
    def __init__(self, id, identity):
        "Constructor for the FileUpload"        

        Component.__init__(self, id, identity)


    #-------------------------------------------------------------------------
    def onAfterIdentityChange(self, identityMappings):
        """
            Handle Knowledge Engine specific actions after the 
            identity of this object has changed.
            
            The identityMappings dictionary holds 
            mappings from oldIdentities -> newIdentities
            (including this object)
                        
        """

        #perform default identity changes
        Component.onAfterIdentityChange(self, identityMappings)


    #-------------------------------------------------------------------------
    def onAfterInstanciation(self):
        """
            Handle Knowledge Engine specific actions after this object 
            has been loaded into memory.  
            
            This is useful for performing backwards compatibility
        """

        #perform default onAfterInstanciation for the parent
        Component.onAfterInstanciation(self)


    #-------------------------------------------------------------------------
    def getDefaultValue(self):
        "Returns the component assessment default value"        
        
        return getattr(self, 'defaultValue', '')


    #-------------------------------------------------------------------------
    def setDefaultValue(self, defaultValue):
        "Set the component assessment default value"        
        self.defaultValue = defaultValue


    #-------------------------------------------------------------------------
    def getComponentProperty(self, identity, propertyName, assessment = None):
        """
            Returns the value of the specified property, given the
            identity of the component (which may be used to lookup states) and
            an assessment.

            RETURNS: None if the property doesn't exist
        """

        if propertyName == "value":
            return assessment.getComponentState(identity).getValue()
        else:
            return Component.getComponentProperty(self, identity, propertyName, assessment)


    #-------------------------------------------------------------------------
    def download(self,  REQUEST=None, **args):
        """
            Download the file
            
            TODO:   Need to integrate KE permissioning into this. For now it
                    just handles not being logged in i.e. the Anonymous User
        """

        # If we didn't get here via a request, don't do anything
        if not REQUEST:
            return
        
        # Ensure the user has access to the model and at least one repository
        # including the personal repository
        user = self.knowledgeengine_userproviderservice.getUser(REQUEST.get('AUTHENTICATED_USER').getId())
        model = self.getModel()
        if not (model.isAccessibleByUser(user) or model.hasUserAccessibleRepositories(user)):
            return

        # Make sure we have an assessmentIdentity before proceeding
        assessmentIdentity = REQUEST.get('assessmentIdentity', None)
        if assessmentIdentity:

            # Load the specific assessment data
            storageService = self.knowledgeengine_assessmentstorageservice
            assessment = storageService.loadAssessment(assessmentIdentity, [self.getModel()])
            if assessment:

                securityService = self.knowledgeengine_securityservice
                downloadAllowed = False

                # Split apart the component data
                componentState = assessment.getComponentState(self.getIdentity())
                if componentState.getValue():
                    (basename, content_type, filename, file_size) = componentState.getValue().split(',')
        
                    # Load the file data
                    full_filename = "%s/%s" % (self.getPropertyValue('filesystemDirectory'), basename)
                    uploaded_file = open(full_filename, "rb")
                    file_contents = uploaded_file.read()
                    uploaded_file.close()

                    # Finally, ensure the correct HTTP headers are set so that the data
                    # is downloaded
                    setHeader = REQUEST.RESPONSE.setHeader
                    setHeader( 'content-type', content_type )
                    setHeader( 'content-disposition', 'attachment;filename=%s' % (filename, ) )

                    return file_contents
        
        # No file uploaded. Don't to anything
        return

    #---------------------------------------------------------------#
    # BEGIN: Assessible Implementation                              #
    #---------------------------------------------------------------#
    def getAssessmentValueType(self):
        """
            ABSTRACT: Returns the type of the value that is to be stored
            for this component when it's assessment state is persisted.
            
            NOTE 1: See AssessmentComponentValueTypes for a list of defined contants       
            NOTE 2: This method MUST be overriden in the implementing
            class to specify the correct type for value persistence
            
            RETURN TYPE: String
        """
        
        return st_STRING


    #-------------------------------------------------------------------------
    def getDefaultAssessmentState(self, assessment):
        """
            ABSTRACT: Constructs and returns a suitable 'default' 
            AssessmentComponentState for the provided Assessment
            instance.

            NOTE: This method MUST be overriden in the implementing
            class to provide a suitable default value for the component
            
            RETURN TYPE: AssessmentComponentState
        """
        
        return AssessmentComponentState(
            assessment.getIdentity(),
            assessment.getModelIdentity(),
            self.getIdentity(),
            self.getAssessmentValueType(),
            self.getDefaultValue(),
            revision=assessment.getRevision()
        )


    #-------------------------------------------------------------------------
    def updateAssessmentState(self, assessment, formValue, skipMandatory=False ):
        """
            ABSTRACT: Updates the specified assessment using
            the provided HTTP REQUEST formValue.
            
            NOTE: This method MUST be overriden in the implementing
            class to suitably update the component state
        """
        
        component_value = ""
        filename = getattr(formValue, 'filename', None)
        if filename:
            file_contents = formValue.read()
            file_size = len(file_contents)

            # Check that the file is within our set bounnds
            if file_size > (self.getPropertyValue('maximumFilesize') * 1024):
                raise FileUploadExceedsMaximumSizeException('Uploaded file is too big', component=self)
        
        
            # Now check that the file is of an acceptable MIME type
            content_type    = formValue.headers['Content-Type'].lower()
            allowed_types   = self.getPropertyValue('allowedMimeTypes')
            
            # If allowed_types is specified, only allow the desired mime types through (If not, let anything through)
            if allowed_types:            
                clean_types = map(lambda t: t.lower(), allowed_types.split(','))
                
                if (content_type not in clean_types) and ( content_type.replace("x-","") not in clean_types):
                    raise MimeTypeIncorrectException('MIME Type incorrect - %s' % (content_type), component=self)
        
        
            # The directory where the uploaded files will live and prosper
            #
            # TODO: Check that we have write permissions on this directory
            storage_directory = self.getPropertyValue('filesystemDirectory')
        
            # The filename will be composed of the assessmentIdentity and componentId. This is
            # stored as the value.
            #
            # 20061221 johnm    If the MIME type is pre-registered, guess the file suffix
            if MIME_TYPES.has_key(content_type):
                suffix = MIME_TYPES[content_type]
            else:
                suffix = filename.split('.').pop()
                
            basename = "%s_%s.%s" % (assessment.getIdentity(), self.getIdentity(), suffix)

            # Create a new file at the location given by the storage directory
            # and the constructed filename
            full_filename = "%s/%s" % (storage_directory, basename)
            uploaded_file = open(full_filename, "wb")
            uploaded_file.write(file_contents)
            uploaded_file.close()

            # 20061221 johnm    Try to register the extension / suffix
            registerFileExtension(suffix, FSFile)
        
            # Construct the file details for storage
            file_details = []
            file_details.append(basename)
            file_details.append(content_type)
            file_details.append(formValue.filename.replace(',', '_'))
            file_details.append(file_size)
            
            component_value = ','.join(map(lambda value: str(value), file_details))
        
            

        #get the current assessment state for the component from the assessment
        state = assessment.getComponentState(self.getIdentity())

        # Check that we're not overwriting the previous details with nothing
        #
        # TODO: How do we empty an uploaded file?
        previous_value = state.getValue()
        if (not component_value) and previous_value and (formValue != "CLEAR"):
            return

        Component.updateAssessmentState(self, assessment, component_value)
                
        #update the current state
        state.setValue(component_value)
        
        # If a searchIndex is defined, add that info to the component state
        searchIndex = getattr(self, 'searchIndex', False)
        if searchIndex:
            state.setSearchIndex(searchIndex)
            state.setSearchValue(component_value)
        
        #update the assessment
        assessment.setIsModified(true)
    #---------------------------------------------------------------#
    # END: Assessible Implementation                                #
    #---------------------------------------------------------------#

    
    #---------------------------------------------------------------#
    # BEGIN: KERLExtensions Implementation                          #
    #---------------------------------------------------------------#
    def compileKERLtoJavaScript(self, expression):
        """
            Returns a Python string that represents a JavaScript
            expression, which may be used to evaluate the status
            of the component class specific KERL expression.

            NOTE: this method is primarily used to produce
            JavaScript expressions that form the basis of the
            client-side showif technology.
            
            RETURN TYPE: String         
        """
        
        #example: cond="['txt1','contains', value]" returns 'document.getElementById("txt1").value.indexOf(value)>=0'
        #example: cond="['txt1','doesntcontain', value]" returns 'document.getElementById("txt1").value.indexOf(value)<0'
        #example: cond="['txt1','equals', value]" returns 'document.getElementById("txt1").value==value'
        #example: cond="['txt1','notequal', value]" returns 'document.getElementById("txt1").value!=value'

        #get the identity (which is the identity of itself)        
        identity = self.getIdentity()

        #get the operator
        opr = expression[1]

        #get the value
        value = asJavaScript(expression[2])

        #determine the result
        result = "document.getElementById(\"" + identity + "\").value"
        
        if opr == "contains":
            return result + ".indexOf(\"" + value + "\")>=0"
        
        elif opr == "doesntcontain":
            return result + ".indexOf(\"" + value + "\")<0"
        
        elif opr == "equals":
            return result + "==\"" + value + "\""
        
        elif opr == "doesntequal":
            return result + "!=\"" + value + "\""
        
        else:
            raise InvalidKerlOperator, opr
            #return "true"


    #-------------------------------------------------------------------------
    def compileKERLtoEnglish(self, expression):
        """
            Returns a string representing an 'English' version
            of the component class specific KERL expression.

            NOTE: this method is primarily used to produce
            English descriptions of KERL expressions that may be 
            used to describe things like AssessmentFilters
            
            RETURN TYPE: String         
        """
        
        return ""


    #-------------------------------------------------------------------------
    def evaluate(self, expression, assessment):
        """
            Evalutes the component class specific KERL expression
            using the provided assessment.

            NOTE: this method is primarily used to produce
            JavaScript expressions that form the basis of the
            client-side showif technology.

            RETURN TYPE: Boolean         
        """
        
        #get the identity
        identity = self.getIdentity()

        #get the operator
        opr = expression[1]

        #get the value
        value = asJavaScript(expression[2])

        #get the stored state
        state = assessment.getComponentState(identity)

        #evaluate the expression
        if opr == "contains":
            return state.getValue().find(value) >= 0
        
        elif opr == "doesntcontain":
            return state.getValue().find(value) == -1
        
        elif opr == "equals":
            return state.getValue() == value
        
        elif opr == "doesntequal":
            return state.getValue() != value
        
        else:
            raise InvalidKerlOperator, opr
            #return true


    #-------------------------------------------------------------------------
    def getDependencies(self, expression):
        """
            Returns a list of components that are required to
            evaluate the component class specific KERL expression.

            NOTE: Includes the specific component itself.

            RETURN TYPE: {Component} indexed by Component.getIdentity()
       """

        return {self.getIdentity():self}


    #-------------------------------------------------------------------------
    
    def updateKERLIdentities(self, expression, identityMappings):
        """
            Returns the expression with the appropriate old identities
            (as identified by the identityMappings) replaced with 
            new identities.
        """
        
        #get the component identity from the expression (first element)
        identity = expression[0]
        
        #is the component in the identityMappings?
        if identityMappings.has_key(identity):
            expression[0] = identityMappings[identity]
        
        return expression
        
        
        
    #---------------------------------------------------------------#
    # END: KERLExtensions Implementation                            #
    #---------------------------------------------------------------#


#####################################################################
## FileUploadExceedsMaximumSizeException Definition
class FileUploadExceedsMaximumSizeException(ValidationException):
    """
        A validation error that occurs when the uploaded file's size exceeds
        the allowable property value
    """
    pass


class MimeTypeIncorrectException(ValidationException):
    """
        A validation error that occurs when the MIME type of an uploaded file in incorrect
    """
    pass


#####################################################################
# Class Initialisation
#
# Register Component Information with the Knowledge Engine
registerGenericObjectClass(FileUpload)
